package org.fmi.lje.graphics.model.impl;

import static org.fmi.lje.graphics.bcel.BcelUtil.asString;

import java.awt.Color;
import java.awt.FontMetrics;
import java.net.URLClassLoader;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.util.ClassLoaderRepository;
import org.apache.log4j.Logger;
import org.fmi.lje.graphics.diagrams.ClassHierarchyDiagram;
import org.fmi.lje.graphics.model.Association;
import org.fmi.lje.graphics.model.Clazz;
import org.fmi.lje.graphics.model.Inheritance;
import org.fmi.lje.graphics.model.Relationship;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;

public class BcelClazz extends Clazz {

	private static final Logger log = Logger.getLogger(BcelClazz.class);

	private Set<Inheritance> subclassInheritances = new HashSet<Inheritance>();
	private Set<Inheritance> superclassInheritances = new HashSet<Inheritance>();

	private Set<BcelClazz> superclasses;
	private Set<BcelClazz> subclasses;

	private Set<Association> associations = new HashSet<Association>();

	private JavaClass target;

	private Class<?> oClass;
	private ClassLoader oClassLoader;

	@Override
	public void setTarget(Class<?> target) {
		try {
			oClass = target;
			oClassLoader = target.getClassLoader();

			ClassLoaderRepository repository = new ClassLoaderRepository(oClassLoader);

			this.target = repository.loadClass(target);
		} catch (ClassNotFoundException e) {
			log.error(e);
		}
	}

	private void setTarget(JavaClass target, ClassLoader oClassLoader) throws ClassNotFoundException {
		this.target = target;

		this.oClassLoader = oClassLoader;
		this.oClass = oClassLoader.loadClass(target.getClassName());
	}

	private BcelClazz createSibling(JavaClass sibling, ClassLoader oClassLoader) {
		BcelClazz zibling = null;

		if (!sibling.getClassName().contentEquals(Object.class.getCanonicalName())) {
			try {
				if (getParent() instanceof ClassHierarchyDiagram) {
					ClassHierarchyDiagram parent = (ClassHierarchyDiagram) getParent();

					if (parent.getShapeById(sibling.toString()) != null) {
						zibling = (BcelClazz) parent.getShapeById(sibling.toString());
					}
				}

				if (zibling == null) {
					zibling = new BcelClazz();
					zibling.setTarget(sibling, oClassLoader);
					zibling.setParent(getParent());
				}
			} catch (ClassNotFoundException e) {
				log.error(e);

				throw new RuntimeException(e);
			}
		}

		return zibling;
	}

	private BcelClazz createSibling(Class<?> sibling) {
		BcelClazz zibling = null;

		if (!sibling.equals(Object.class)) {
			if (getParent() instanceof ClassHierarchyDiagram) {
				ClassHierarchyDiagram parent = (ClassHierarchyDiagram) getParent();

				if (parent.getShapeById(sibling.toString()) != null) {
					zibling = (BcelClazz) parent.getShapeById(sibling.toString());
				}
			}

			if (zibling == null) {
				zibling = new BcelClazz();
				zibling.setTarget(sibling);
				zibling.setParent(getParent());

				((ClassHierarchyDiagram) getParent()).addShape(zibling, sibling.toString());
			}
		}

		return zibling;
	}

	@Override
	public int getHeight() {
		FontMetrics fm = getGraphics().getFontMetrics();

		int h = fm.getAscent();
		int height = h;

		if (target.isInterface()) {
			height += h;
		} else {
			if (target.isAbstract()) {
				height += h;
			}
		}

		height += target.getFields().length * h;
		height += target.getMethods().length * h;
		height += 3 * h;
		height += 6 * PADDING;

		return height;
	}

	@Override
	public int getWidth() {
		FontMetrics fm = getGraphics().getFontMetrics();
		int width = fm.stringWidth(target.getClassName());

		for (Field field : target.getFields()) {
			if (fm.stringWidth(asString(field)) > width) {
				width = fm.stringWidth(asString(field));
			}
		}

		for (Method method : target.getMethods()) {
			if (fm.stringWidth(asString(method)) > width) {
				width = fm.stringWidth(asString(method));
			}
		}

		width += 4 * PADDING;

		return width;
	}

	@Override
	public void draw() {
		getGraphics().setFont(getFont());
		getGraphics().setColor(Color.black);

		FontMetrics fm = getGraphics().getFontMetrics();

		int h = fm.getAscent();
		int x = getX();
		int y = getY() + h + PADDING;

		if (target.isInterface()) {
			getGraphics().drawString("<< interface >>", x + PADDING, y);

			y += h + PADDING;
		}

		getGraphics().drawString(target.getClassName(), x + PADDING, y);
		y += h + PADDING;

		if (target.isAbstract() && !target.isInterface()) {
			getGraphics().drawString("{ abstract }", x + PADDING, y);

			y += h + PADDING;
		}

		getGraphics().drawLine(x, y, x + getWidth(), y);
		y += h + PADDING;

		for (Field field : target.getFields()) {
			getGraphics().drawString(asString(field), x + PADDING, y);
			y += h;
		}

		getGraphics().drawLine(x, y, x + getWidth(), y);
		y += h + PADDING;

		for (Method method : target.getMethods()) {
			getGraphics().drawString(asString(method), x + PADDING, y);
			y += h;
		}

		getGraphics().drawRect(getX(), getY(), getWidth(), getHeight());
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((target == null) ? 0 : target.hashCode());
		return result;
	}

	public Collection<BcelClazz> getSuperClassHierarchy() {
		if (superclasses == null) {
			superclasses = new HashSet<BcelClazz>();

			try {
				if (target.getSuperClass() != null) {
					BcelClazz superclass = this.createSibling(target.getSuperClass(), oClassLoader);

					if (superclass != null) {
						superclass.subclassInheritances.add(new Inheritance(superclass, this));
						superclassInheritances.add(new Inheritance(superclass, this));

						superclasses.add(superclass);

						superclasses.addAll(superclass.getSuperClassHierarchy());
						superclasses.addAll(superclass.getSubclassHierarchy());
					}
				}

				for (JavaClass iface : target.getInterfaces()) {
					BcelClazz intface = this.createSibling(iface, oClassLoader);

					intface.subclassInheritances.add(new Inheritance(intface, this));
					superclassInheritances.add(new Inheritance(intface, this));

					superclasses.add(intface);

					superclasses.addAll(intface.getSuperClassHierarchy());
					superclasses.addAll(intface.getSubclassHierarchy());
				}
			} catch (ClassNotFoundException e) {
				log.error(e);
			}
		}

		return superclasses;
	}

	public Collection<BcelClazz> getSubclassHierarchy() {
		if (subclasses == null) {
			subclasses = new HashSet<BcelClazz>();

			if (oClass != Object.class) {
				ConfigurationBuilder cb = new ConfigurationBuilder();
				//cb.filterInputsBy(new FilterBuilder().include(FilterBuilder.prefix("org.fmi.lje")));

				if (oClassLoader instanceof URLClassLoader) {
					cb.setUrls(((URLClassLoader) oClassLoader).getURLs());
				}
				cb.setScanners(new SubTypesScanner());

				Reflections reflections = new Reflections(cb);
				Set<?> subTypesOf = reflections.getSubTypesOf(oClass);

				for (Object obj : subTypesOf) {
					ClassLoaderRepository repository = new ClassLoaderRepository(oClassLoader);

					try {
						JavaClass temp = repository.loadClass((Class<?>) obj);

						boolean found = false;
						for (JavaClass iface : temp.getInterfaces()) {
							if (iface.equals(target)) {
								found = true;

								break;
							}
						}

						if (temp.getSuperClass().equals(target)) {
							found = true;
						}

						if (found) {
							BcelClazz subclass = this.createSibling((Class<?>) obj);

							subclass.superclassInheritances.add(new Inheritance(this, subclass));
							subclassInheritances.add(new Inheritance(this, subclass));

							subclasses.add(subclass);

							subclasses.addAll(subclass.getSuperClassHierarchy());
							subclasses.addAll(subclass.getSubclassHierarchy());
						}
					} catch (ClassNotFoundException e) {
						log.error(e);
					}
				}
			}
		}

		return subclasses;
	}

	@Override
	public String toString() {
		return oClass != null ? oClass.toString() : "null";
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		BcelClazz other = (BcelClazz) obj;
		if (target == null) {
			if (other.target != null)
				return false;
		} else if (!target.equals(other.target))
			return false;
		return true;
	}

	protected JavaClass getTarget() {
		return target;
	}

	// return an immutable instance
	public Collection<? extends Relationship> getSuperclasses() {
		return new HashSet<Inheritance>(superclassInheritances);
	}

	// return an immutable instance
	public Collection<? extends Relationship> getSubclasses() {
		return new HashSet<Inheritance>(subclassInheritances);
	}

	// return an immutable instance
	public Collection<? extends Relationship> getAssociations() {
		return new HashSet<Association>(associations);
	}
}
